{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "import ipywidgets as widgets"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Separating the logic from the widgets\n",
    "\n",
    "A key principle in designing a graphical user interface is to separate the logic of an application from the graphical widgets the user sees. For example, in the super-simple password generator widget, the basic logic is to construct a sequence of random letters given the length. Let's isolate that logic in a function, without any widgets. This function takes a password length and returns a generated password string."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def calculate_password(length):\n",
    "    import string\n",
    "    import secrets\n",
    "    \n",
    "    # Gaenerate a list of random letters of the correct length.\n",
    "    password = ''.join(secrets.choice(string.ascii_letters) for _ in range(length))\n",
    "\n",
    "    return password"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Test out the function a couple times in the cell below with different lengths. Note that unlike our first pass through this, you can test this function without defining any widgets. This means you can write tests for just the logic, use the function as part of a library, etc."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'ppcdOhADeC'"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "calculate_password(10)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## The Graphical Controls\n",
    "\n",
    "The code to build the graphical user interface widgets is the same as the previous iteration."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4f6597e4ec5d43c1984e61987fa0a49b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "A Jupyter Widget"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "helpful_title = widgets.HTML('Generated password is:')\n",
    "password_text = widgets.HTML('No password yet')\n",
    "password_text.layout.margin = '0 0 0 20px'\n",
    "password_length = widgets.IntSlider(description='Length of password',\n",
    "                                   min=8, max=20,\n",
    "                                   style={'description_width': 'initial'})\n",
    "\n",
    "password_widget = widgets.VBox(children=[helpful_title, password_text, password_length])\n",
    "password_widget"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Connecting the logic to the widgets\n",
    "\n",
    "When the slider `password_length` changes, we want to call `calculate_password` to come up with a new password, and set the value of the widget `password` to the return value of the function call.\n",
    "\n",
    "`update_password` takes the change from the `password_length` as its argument and sets the `password_text` with the result of `calculate_password`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def update_password(change):\n",
    "    length = int(change.new)\n",
    "    new_password = calculate_password(length)\n",
    "    \n",
    "    # NOTE THE LINE BELOW: it relies on the password widget already being defined.\n",
    "    password_text.value = new_password\n",
    "    \n",
    "password_length.observe(update_password, names='value')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that the connection is made, try moving the slider and you should see the password update."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "4f6597e4ec5d43c1984e61987fa0a49b",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "A Jupyter Widget"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "password_widget"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Benefits of separating concerns\n",
    "\n",
    "Some advantages of this approach are:\n",
    "\n",
    "+ Changes in `ipywidgets` only affect your controls setup.\n",
    "+ Changes in functional logic only affect your password generation function. If you decide that a password with only letters isn't secure enough and decide to add some numbers and/or special characters, the only code you need to change is in the `calculate_password` function.\n",
    "+ You can write unit tests for your `calculate_password` function  -- which is where the important work is being done -- without doing in-browser testing of the graphical controls."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Using interact\n",
    "\n",
    "Note that using interact to build this GUI also emphasizes the separation between the logic and the controls. However, interact also is much more opinionated about how the controls are laid out: controls are in a vbox above the output of the function. Often this is great for a quick initial GUI, but is restrictive for more complex GUIs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "8a10e24d23af40c38aac7af707c35929",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "A Jupyter Widget"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from ipywidgets import interact\n",
    "from IPython.display import display\n",
    "interact(calculate_password, length=(8, 20));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can make the interact a bit nicer by printing the result, rather than just returning the string. This time we use `interact` as a decorator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "f12a8bf7afde4ac1a158b3ee188e785e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "A Jupyter Widget"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "@interact(length=(8, 20))\n",
    "def print_password(length):\n",
    "    print(calculate_password(length))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "widgets-tutorial",
   "language": "python",
   "name": "widgets-tutorial"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
